########################################################################
#  Pyfm - personal finance data-base
#  Copyright (C) 2007,8 Ehud Ben-Reuven
#  udi@benreuven.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
########################################################################
"""export OFX or QFX files."""
from model.modellib import *
from model.tagsyntax import DTASOF
from model.trnlib import gettransaction_filter, gettransactions, trntotal
from obj.flat import stagvalue
import lib.dirs
from lib.quoteinterface import getFXrate

from htmlentitydefs import codepoint2name,entitydefs 

unichr2entity = dict((unichr(code), u'&%s;' % name)
    for code,name in codepoint2name.iteritems()
    if code!=38) # exclude "&"

chr2entity = dict((chr(code), '&%s;' % name)
    for code,name in codepoint2name.iteritems()
    if code<256 and chr(code)!='&')

def htmlescape(text, du=unichr2entity, dc=chr2entity):
    if isinstance(text,unicode):
        d=du
        if u"&" in text:
            text = text.replace(u"&", u"&amp;")
    else:
        d=dc
        if "&" in text:
            text = text.replace("&", "&amp;")
    for key, value in d.iteritems():
        if key in text:
            text = text.replace(key, value)
    return text

def ptran(Secid,fitid,dttrade,dtsettle,memo,units,price,total,incometype,cur=None,curdef=None):
    xrate=getFXrate(dttrade,cur,curdef)

    sTransaction=''
    if units!=None:
        if price!=None:
            if units>0:
                sTransaction+="<BUYSTOCK>\n<INVBUY>\n"
            else:
                sTransaction+="<SELLSTOCK>\n<INVSELL>\n"
        else:
            sTransaction+="<TRANSFER>\n"
    else:
        sTransaction+="<INCOME>\n"
                            
    sTransaction+="<INVTRAN>\n"
    sTransaction+="<FITID>%d\n"%fitid
    sTransaction+="<DTTRADE>%s\n"%dttrade.strfull()
    sTransaction+="<DTSETTLE>%s\n"%dtsettle.strfull()
    if memo: sTransaction+="<MEMO>%s\n"%memo
    sTransaction+="</INVTRAN>\n"
                    
    sTransaction+=Secid
    if units!=None and price==None:
        sTransaction+="<SUBACCTSEC>CASH\n"
    if incometype!=None:
        sTransaction+="<INCOMETYPE>%s\n"%incometype
    if units!=None:
        sTransaction+="<UNITS>%f\n"%units
    if price!=None:
        sTransaction+="<UNITPRICE>%f\n"%(price*xrate)
    if units!=None and price==None:
        sTransaction+="<TFERACTION>%s\n"%("IN" if units>0 else "OUT")
        sTransaction+="<POSTYPE>LONG\n" # FIXME this is not always correct! but the tag is mandatory
    if total!=None:
        sTransaction+="<TOTAL>%f\n"%(total*xrate)

    # Yaak...there is also the INCOME case below in which currency is added but
    # then it has to be after SUBACCTFUND.
    if units!=None and price!=None:
        sTransaction+=pcur(cur,dttrade,curdef)

    if not (units!=None and price==None):
        sTransaction+="<SUBACCTSEC>CASH\n"
    if total!=None: sTransaction+="<SUBACCTFUND>CASH\n"
                    
    if units!=None:
        if price!=None:
            if units>0:
                sTransaction+="</INVBUY>\n<BUYTYPE>BUY\n</BUYSTOCK>\n"
            else:
                sTransaction+="</INVSELL>\n<SELLTYPE>SELL\n</SELLSTOCK>\n"
        else:
            sTransaction+="</TRANSFER>\n"
    else:
        sTransaction+=pcur(cur,dttrade,curdef)
        sTransaction+="</INCOME>\n"
                            
    return sTransaction

def pcur(cur,date,curdef):
    if not cur: return ''
    if cur==curdef: return ''
    return "<ORIGCURRENCY>\n<CURRATE>%f\n<CURSYM>%s\n</ORIGCURRENCY>\n"%(getFXrate(date,cur,curdef),cur)


def export_investment_ofx(fout,holdings,statement_filter,transaction_filter,dtasof,dtstart,dtend,brokerid,acctid,accttype,quickenBID=None,language=None,curdef='USD'):
    """Export to OFX v1 format
To test interoperability with MS Money, download and install OFXAnalyzer located at the bottom of
http://www.microsoft.com/money/fi/tools.mspx
and run it on the output file.
If an error appears, add 11 to the line number

If you want the format to be compatible with Quicken (.QFX file) you have to assign a Bank IDentifier number in quickenBID.
With this number, you pretend that the information was generated by that bank.
Legal values should be taken from the right most column of the file located at:
C:\Documents and Settings\All Users\Application Data\Intuit\Quicken\Inet\Common\Localweb\Banklist\fidir.zip
You can pick up any bank you like but it has to support web connection.
The bank should appear in the file to support investment or checking depending on what you used in accttype
"""
    assert(accttype=='INVESTMENT') # CHECKING not implemented
    r={'DTASOF':dtasof.strfull(),'BROKERID':brokerid,'ACCTID':acctid,'CURDEF':curdef}
    sTransaction=''
    sStatement=''
    sAsset=''
        
    for h in holdings:
        a=h['Asset'][0] # FIXME a holding might have more than one sAsset, which one to use?
        assert isinstance(a,Asset)
        Secid="<SECID>\n<UNIQUEID>%s\n<UNIQUEIDTYPE>CUSIP\n</SECID>\n"%a.getcusip()
        hs=getholdingsources(h)
        #if hs: continue
        statement=getstatement(statement_filter,h=h,hs=hs)
        if statement: 
            units=statement['UNITS'][0]
            dtprice,unitprice,cur=gethistory(h,dtend)
            unitprice=float("%.4f"%unitprice)
            unitprice*=getFXrate(dtprice,cur,curdef)
            sStatement+="<POSSTOCK>\n<INVPOS>\n"
            sStatement+=Secid
            sStatement+="<HELDINACCT>CASH\n<POSTYPE>LONG\n" #FIXME
            sStatement+="<UNITS>%f\n"%units
            sStatement+="<UNITPRICE>%f\n"%unitprice
            mktval=units*unitprice
            sStatement+="<MKTVAL>%f\n"%mktval
            sStatement+="<DTPRICEASOF>%s\n"%dtprice.strfull()
            sStatement+=pcur(cur,dtprice,curdef)
            sStatement+="</INVPOS>\n</POSSTOCK>\n"

        transactions=gettransactions(transaction_filter,h=h,hs=hs)
        for t in transactions:
            fitid=t['_LINE_INDEX_'][0]
            dtsettle=t.DTSETTLE[0]
            dttrade=t.DTTRADE[0] if t.DTTRADE else dtsettle
            memo=htmlescape(' '.join(t.MEMO))
            memo=memo[:255] # FIXME this can fall on an HTML escape sequence
            fees = t.FEES[0] if t.FEES else None
                        
            cur = t['CURRENCY']
            if cur:
                cur=cur[0]
            else:
                cur = holdingcur(h)                    

            if t.UNITS: # Buy or sell or split or transfer
                units=t.UNITS[0]
                if t.UNITPRICE: # Buy/Sell
                    price=float("%.4f"%t.UNITPRICE[0])
                    total = -units*price
                else: # Transfer
                    price=None
                    total=None
                sTransaction+=ptran(Secid,fitid*10,dttrade,dtsettle,memo,units,price,total,None,cur,curdef)

            if t.INTEREST: # Interest or dividend
                total=t.INTEREST[0]
                trntype=t.TRNTYPE[0]
                if trntype=='INT':
                    incometype='INTEREST'
                elif trntype in ['DIV', 'MISC', 'CGLONG', 'CGSHORT']:
                    incometype=trntype
                elif trntype=='CG':
                    incometype='CGSHORT' # Dont know if it is long or short so taking higher tax option
                else:
                    incometype='INTEREST' # All others take interest which is maximal tax
                sTransaction+=ptran(Secid,fitid*10+1,dttrade,dtsettle,memo,None,None,total,incometype,cur,curdef)
                                
        if statement or transactions:
            print stagvalue('Holding',h),'Has Statements='+`bool(statement)`,'Has Transactions='+`len(transactions)`
            sAsset+="<STOCKINFO>\n<SECINFO>\n"
            sAsset+=Secid
            secname=a.getEngSecname()
            #if language: secname=secname.decode(language)
            #secname=htmlescape(secname)
            sAsset+="<SECNAME>%s\n"%secname  # SECNAME is mandatory in SECINFO
            ticker=a.getticker()
            if ticker: sAsset+="<TICKER>%s\n"%ticker
            memo=htmlescape(';'.join(a['AMEMO']))
            if memo: sAsset+="<MEMO>%s\n"%memo
            sAsset+="</SECINFO>\n</STOCKINFO>\n"
                
    print >>fout,"""OFXHEADER:100
DATA:OFXSGML
VERSION:102
SECURITY:NONE"""
    if language:
        print >>fout,"""ENCODING:UNICODE
CHARSET:"""
    else:
        print >>fout,"""ENCODING:USASCII
CHARSET:1252"""
                
    print >>fout,"""COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:NONE


<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS>
<CODE>0
<SEVERITY>INFO
</STATUS>
<DTSERVER>%(DTASOF)s
<LANGUAGE>ENG""" % r
    if quickenBID: print >>fout,"<INTU.BID>%s"%str(quickenBID)
    print >>fout,"""</SONRS>
</SIGNONMSGSRSV1>
<INVSTMTMSGSRSV1>
<INVSTMTTRNRS>
<TRNUID>%(DTASOF)s <!-- unique ID -->
<STATUS>
<CODE>0
<SEVERITY>INFO  
</STATUS>
<INVSTMTRS>
<DTASOF>%(DTASOF)s
<CURDEF>%(CURDEF)s
<INVACCTFROM>
<BROKERID>%(BROKERID)s
<ACCTID>%(ACCTID)s
</INVACCTFROM>""" % r
        
    if sTransaction:
        print >>fout,"""<INVTRANLIST>
<DTSTART>%s
<DTEND>%s
%s</INVTRANLIST>""" %(dtstart.strfull(),dtend.strfull(),sTransaction)
    else:
        print 'No transactions'
                        
    if sStatement:
        print >>fout,"""<INVPOSLIST>
%s</INVPOSLIST>""" % sStatement
    else:
        print 'No statements'
                
    print >>fout,"""</INVSTMTRS>
</INVSTMTTRNRS>
</INVSTMTMSGSRSV1>"""
    if sAsset:
        print >>fout,"""<SECLISTMSGSRSV1>
<SECLIST>
%s</SECLIST>
</SECLISTMSGSRSV1>""" % sAsset
    else:
        print 'No asset info'
    print >>fout,"</OFX>"

def export_checking_ofx(fout,holdings,statement_filter,transaction_filter,dtasof,dtstart,dtend,bankid,acctid,accttype,quickenBID=None,language=None,curdef='USD'):
    """Export to OFX v1 format
To test interoperability with MS Money, download and install OFXAnalyzer located at the bottom of
http://www.microsoft.com/money/fi/tools.mspx
and run it on the output file.
If an error appears, add 11 to the line number

If you want the format to be compatible with Quicken (.QFX file) you have to assign a Bank IDentifier number in quickenBID.
With this number, you pretend that the information was generated by that bank.
Legal values should be taken from the right most column of the file located at:
C:\Documents and Settings\All Users\Application Data\Intuit\Quicken\Inet\Common\Localweb\Banklist\fidir.zip
You can pick up any bank you like but it has to support web connection.
The bank should appear in the file to support investment or checking depending on what you used in accttype
"""
    assert(accttype=='CHECKING') # CHECKING not implemented
    if not holdings:
        print "No Holdings"
        return
    
    #r={'DTASOF':dtasof.strfull(),'BANKID':bankid,'ACCTID':acctid,'CURDEF':curdef}
    sTransaction=''
    sStatement=''
    sAsset=''
        
    print >>fout,"""OFXHEADER:100
DATA:OFXSGML
VERSION:102
SECURITY:NONE"""
    if language:
        print >>fout,"""ENCODING:UNICODE
CHARSET:"""
    else:
        print >>fout,"""ENCODING:USASCII
CHARSET:1252"""
                
    print >>fout,"""COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:NONE

<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS>
<CODE>0
<SEVERITY>INFO
</STATUS>
<DTSERVER>%s
<LANGUAGE>ENG""" % dtasof.strfull()
    #print >>fout,"""<FI>
#<ORG>%(ORG)s
#<FID>%(FID)s
#</FI>""" % r
    if quickenBID: print >>fout,"<INTU.BID>%s"%str(quickenBID)
    print >>fout,"""</SONRS>
</SIGNONMSGSRSV1>
<BANKMSGSRSV1>"""
    
    for h in holdings:
        statement=getstatement(statement_filter,h)
        dtprice,unitprice,cur=gethistory(h,dtend)
        if not statement: continue
        curdef1 = curdef if curdef else cur
        try:
            a=h.Account[0].ACCTID[0]
        except:
            a=h.strfull()

        print >>fout,"""<STMTTRNRS>
<TRNUID>%(DTASOF)s <!-- unique ID -->
<STATUS>
<CODE>0
<SEVERITY>INFO  
</STATUS>
<STMTRS>
<CURDEF>%(CURDEF)s
<BANKACCTFROM>
<BANKID>%(BANKID)s
<ACCTID>%(ACCTID)s
<ACCTTYPE>CHECKING
</BANKACCTFROM>""" % {'CURDEF':curdef1,'BANKID':bankid,'ACCTID':a,'DTASOF':dtasof.strfull()}

        print >>fout,"""<BANKTRANLIST>
<DTSTART>%s
<DTEND>%s""" %(dtstart.strfull(),dtend.strfull())
        
        hs=getholdingsources(h)
        transactions=gettransactions(transaction_filter,h=h,hs=hs)
        transactions.reverse()
        for t in transactions:
            total=trntotal(t)
            if not total: continue
            unitprice,cur = getunitpricecur(t)
            dtsettle=t.DTSETTLE[0]
            total *= getFXrate(dtsettle,cur,curdef)
            
            fitid=t['_LINE_INDEX_'][0]*10
            memo=htmlescape(' '.join(t.MEMO))
            memo=memo[:255] # FIXME this can fall on an HTML escape sequence            

            name=htmlescape(' '.join(t.NAME))
            name=name[:255] # FIXME this can fall on an HTML escape sequence            

            if t.UNITS: # Buy or sell or split or transfer
                units=t.UNITS[0]
                print >>fout,"<STMTTRN>"
                if units>0:
                    print >>fout,"<TRNTYPE>CREDIT"
                else:
                    print >>fout,"<TRNTYPE>DEBIT"
                print >>fout,"""<DTPOSTED>%s
<TRNAMT>%.2f
<FITID>%d""" % (dtsettle.strfull(),units,fitid)
                if name: print >>fout,"<NAME>%s" % name
                if memo: print >>fout,"<MEMO>%s" % memo
                print >>fout,"</STMTTRN>" 
        print >>fout,"</BANKTRANLIST>"

        units=statement['UNITS'][0]
        unitprice*=getFXrate(dtprice,cur,curdef)
        print >>fout,"""<LEDGERBAL>
<BALAMT>%.2f
<DTASOF>%s
</LEDGERBAL>""" % (units,dtend.strfull())

                
        print >>fout,"""</STMTRS>
</STMTTRNRS>"""

    print >>fout,"""</BANKMSGSRSV1>
</OFX>"""

def do_export_ofx(fout,s,dtstart,dtend,line,brokerid,acctid):
    """export EXT=<format> <flat-line>
s - Possible options:
 * `ofx` - OFX v1 format to be used by MS Moeny.
 * `qfx` - OFX v1 file to be used by Quicken. In this case you must also specifiy a value for BID
dtstart/end - Information regarding transactions, statements and history are limited to be between DTSTART (oldest date by default) and DTEND (NOW by default).
line - The flat line contain parameters that control what is being exported:
 * FILE - You can specify the full-path of where you want your exported file to appear.
By default, output will be written to $PYFM_DATA/outputs/<ACCTID>-<DTEND>-<DTSTART>.<EXT>
where <DTEND> and <DTSTART> will be represented by a six digit number.  
 * ACCTID - All holding, transactions and statement information are made to look as if it came from <ACCTID> in the output file.
Default: a common real ACCTID to all Holdings will be used.
 * ACCTTYPE - CHECKING or INVESTMENT (default.) This can generate different structure to be used in the output file. If CHECKING then only CHECKING holdings will be used and if INVESTMENT all other holdings will be used.
 * CURDEF - Default: Common value to all holdings will be used. All prices/values will be converted to it.
 * Holding - Select holdings to be exported. Default: all
 * BID - Bank Identifier that you pretend to have generated the file.
   This tag is used and must be specified when `EXT=qfx` (exporting to Quicken).
   Legal values should be taken from the right most column of the file located at:
   `C:\Documents and Settings\All Users\Application Data\Intuit\Quicken\Inet\Common\Localweb\Banklist\fidir.zip`
   You can pick up any bank you like but it has to support web connection and,
   depending on your assignment to ACCTTYPE, the bank should appear in the file to support investment or checking.
"""
    assert(s in ['ofx','qfx'])
    quickenBID = line.pop('BID',[None])[0]
    if s=='qfx':
        if not quickenBID: raise Exception('You must specify BID')
    else:
        if quickenBID:
            print 'BID is just for Quicken files'
            quickenBID=None
        
    accttype=line.pop('ACCTTYPE',['INVESTMENT'])[0]
    curdef=line.pop('CURDEF',['USD'])[0]
    checking_accounts=line.pop('-CHECKING',None)!=None      
    dtasof=DTASOF('NOW')
      
    holdings=getholdings(line)
    if accttype=='CHECKING':
        holdings=[h for h in holdings if h.ischecking()]
    else:
        if not checking_accounts:
            holdings=[h for h in holdings if not h.ischecking()]
    if not holdings:
        print "No Holding"
        return
    statement_filter=getstatement_filter(line,dtasof)       
    transaction_filter=gettransaction_filter(line,dtstart,dtend)
        
    if accttype=='CHECKING':
        export_checking_ofx(fout,holdings,statement_filter,transaction_filter,dtasof,dtstart,dtend,brokerid,acctid,accttype,quickenBID,curdef=curdef)
    else:
        export_investment_ofx(fout,holdings,statement_filter,transaction_filter,dtasof,dtstart,dtend,brokerid,acctid,accttype,quickenBID,curdef=curdef)
